# 文件：main.py
import re
import socket
import sys
from pathlib import Path
from typing import List

import unicodedata
import uvicorn
from fastapi import FastAPI, UploadFile, File, HTTPException, Request
from fastapi.responses import JSONResponse,HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

# =============== 配置常量 ===============
MAX_FILE_SIZE = 16 * 1024 * 1024  # 16MB限制
ALLOWED_EXTENSIONS = {'.txt', '.jpg', '.png', '.docx', '.xlsx', '.pptx', '.pdf', '.zip', '.rar'}  # 可选的文件类型白名单

# =============== 路径工具 ===============
def get_exe_dir() -> Path:
    """获取可执行文件所在目录（兼容打包环境）"""
    return Path(sys.executable).parent if getattr(sys, 'frozen', False) else Path(__file__).parent

def get_resource_path(relative_path: str) -> Path:
    """获取资源文件的绝对路径（兼容PyInstaller打包）"""
    return Path(sys._MEIPASS) / relative_path if hasattr(sys, '_MEIPASS') else get_exe_dir() / relative_path

# =============== 安全工具 ===============
def sanitize_filename(filename: str) -> str:
    """
    安全处理文件名：
    1. 保留多语言字符（中文/俄文等）
    2. 移除路径分隔符和危险字符
    示例：
    "测试/文件.txt" → "测试文件.txt"
    "document(1).docx" → "document_1.docx"
    """
    # Unicode规范化处理
    clean_name = unicodedata.normalize('NFKC', filename)
    # 移除非法字符
    clean_name = re.sub(r'[\\/:"*?<>|\x00-\x1F]+', '', clean_name)
    # 保留允许的符号
    clean_name = re.sub(r'[^\w\s.\-()\[\]@#]', '', clean_name)
    # 空格转下划线并去除首尾空白
    return re.sub(r'\s+', '_', clean_name).strip()

def is_allowed_file(filename: str) -> bool:
    """检查文件扩展名是否允许（可选的安全增强）"""
    return Path(filename).suffix.lower() in ALLOWED_EXTENSIONS

# =============== 网络工具 ===============
def get_local_ips() -> List[str]:
    """获取本机所有IPv4地址（排除回环地址）"""
    ips = []
    try:
        for interface in socket.getaddrinfo(socket.gethostname(), None):
            ip = interface[4][0]
            if ip.startswith('127.'):
                continue
            if not re.match(r'\d+\.\d+\.\d+\.\d+', ip):
                continue
            ips.append(ip)
        return list(set(ips))  # 去重
    except Exception:
        return ["127.0.0.1"]  # 默认回环地址

# =============== 文件操作 ===============
def ensure_upload_dir(upload_dir: Path) -> None:
    """确保上传目录存在"""
    upload_dir.mkdir(parents=True, exist_ok=True)

def get_unique_filepath(directory: Path, filename: str) -> Path:
    """生成唯一的文件保存路径"""
    counter = 0
    stem = Path(filename).stem
    suffix = Path(filename).suffix
    
    while True:
        candidate = directory / f"{stem}{f'_{counter}' if counter else ''}{suffix}"
        if not candidate.exists():
            return candidate
        counter += 1

# =============== FastAPI应用 ===============
app = FastAPI(title="文件上传服务", version="1.1.0")
app.mount("/static", StaticFiles(directory=get_resource_path("static")), name="static")
templates = Jinja2Templates(directory=get_resource_path("templates"))

# 初始化目录
EXE_DIR = get_exe_dir()
UPLOAD_DIR = EXE_DIR / "uploads"
ensure_upload_dir(UPLOAD_DIR)

@app.get("/", response_class=HTMLResponse)
async def upload_page(request: Request):
    """文件上传页面"""
    return templates.TemplateResponse("index.html", {"request": request})

@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
    """文件上传处理接口"""
    # 安全处理文件名
    if not file.filename:
        raise HTTPException(400, "无效文件名")
    
    clean_name = sanitize_filename(file.filename)
    
    # 文件类型检查（可选）
    if False and not is_allowed_file(clean_name):
        raise HTTPException(400, "不支持的文件类型")
    
    # 生成唯一保存路径
    save_path = get_unique_filepath(UPLOAD_DIR, clean_name)
    
    try:
        # 流式写入文件
        with save_path.open("wb") as buffer:
            while chunk := await file.read(8192):  # 8KB块读取
                buffer.write(chunk)
        
        return JSONResponse({
            "success": True,
            "filename": save_path.name,
            "message": f"文件保存至 {save_path.relative_to(EXE_DIR)}"
        })
        
    except Exception as e:
        # 清理不完整文件
        if save_path.exists():
            save_path.unlink(missing_ok=True)
        
        return JSONResponse({
            "success": False,
            "error": str(e)
        }, status_code=500)

# =============== 启动服务 ===============
def print_startup_info():
    """打印服务启动信息"""
    print("\n" + "="*50)
    print(f"文件上传服务 v{app.version}")
    print("可访问地址:")
    for ip in get_local_ips():
        print(f"  http://{ip}:8000")
    print(f"文件保存位置: {UPLOAD_DIR.relative_to(EXE_DIR)}")
    print("="*50 + "\n")

if __name__ == "__main__":
    print_startup_info()
    uvicorn.run(
        app, 
        host="0.0.0.0", 
        port=8000,
        server_header=False  # 隐藏服务器标识增强安全
    )
